package pa3;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

import pa3.SystemManager;

public class HostImpl extends UnicastRemoteObject implements Host, Runnable {

	/**
	 * 
	 */
	private static final long serialVersionUID = -14019794110865162L;
	
	private HostState hostState;
	private Registry registry;
	private Logger logger;
	
	public HostImpl(HostState hostState) throws RemoteException {
		super();
		this.hostState = hostState;
		
        registry = LocateRegistry.getRegistry("localhost", Registry.REGISTRY_PORT);
        
        try {
			logger = (Logger) registry.lookup(Logger.SERVICENAME);
		} catch (NotBoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}   
	}

	/**
	 * Gets called when the host gets checked. If the checked Host is still alive it
	 * will respond by calling the caller's checkResponse method.
	 */
	@Override
	public void checkHost(Host caller, String callerName) throws RemoteException {
		if(!hostState.isCrashed()){
			//generate a random number between 0 and 100 which 
			//determines if the host will respond to the message
			//or if he will crash
			
			//cancel the schedule timer for the caller
			cancelScheduleTimerForHost(callerName);
			//reset the schedule timer for the caller
			rescheduleCheckForHost(callerName);
			
			Random generator = new Random();
			
			int random = generator.nextInt(100);
			
			if(random >= hostState.getFailureProbability()){
				try{
					
					//send the response message
					caller.checkResponse(hostState.getName());
					logger.incrementMessageCount();
				}catch(RemoteException e){
					//if an exception occurs here, the caller crashed in the meantime
					failureDetected(callerName);
				}
			}
			else{
				crash();
			}
		}
		
	}
	
	/**
	 * This method simulates a crash 
	 */
	public void crash() {
		logEvent("crashed");
		//hostState.resetOtherHosts();
		try {
			logger.logCrash(hostState.getName());
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		hostState.setCrashed(true);
		/*
		if(hostState.getCheckTimers() != null){
			//hostState.cancelCheckTimers();
			cancelCheckTimers();
			hostState.resetCheckTimers();
		}		
		if(hostState.getCheckScheduleTimer() != null){
			hostState.cancelCheckScheduleTimer();
			hostState.resetCheckScheduleTimer();
		}
		*/
		//go to sleep for 15s
		try {
			Thread.sleep(15000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//"reboot" and join the group again
		
		try {
			logger.logRecovery(hostState.getName());
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
			
		hostState.setCrashed(false);
		logEvent("joined the group");
		joinGroup();
		rescheduleChecks();
	}

	private void cancelCheckTimers() {
		//synchronized(hostState.getCheckTimers()){
			if(hostState.getCheckTimers() != null){
				Iterator<String> iter = hostState.getCheckTimers().keySet().iterator();
				while(iter.hasNext()){
					String hostName = iter.next();
					hostState.cancelTimerFor(hostName);
					//hostState.getCheckTimers().get(hostName).cancel();
				}
			}
		//}		
	}
	private void cancelCheckScheduleTimers() {
		//synchronized(hostState.getCheckTimers()){
			if(hostState.getCheckScheduleTimers() != null){
				Iterator<String> iter = hostState.getCheckScheduleTimers().keySet().iterator();
				while(iter.hasNext()){
					String hostName = iter.next();
					hostState.cancelScheduleTimerFor(hostName);
					//hostState.getCheckTimers().get(hostName).cancel();
				}
			}
		//}		
	}

	/** 
	 * This method connects the host to the other hosts
	 */
	private synchronized void joinGroup() {
		hostState.setCrashed(false);
		try {
			registry = LocateRegistry.getRegistry("localhost", Registry.REGISTRY_PORT);
			SystemManager systemManager = (SystemManager) registry.lookup(SystemManager.SERVICENAME);            
			hostState.setOtherHosts(systemManager.register(this, hostState.getName()));
			hostState.getOtherHosts().remove(hostState.getName());
			
			logger.register(hostState.getName());
			
			//tell the other hosts that it's here now
			Iterator<String> iter = ((HashMap<String, Host>)hostState.getOtherHosts().clone()).keySet().iterator();
			while(iter.hasNext()){
				String otherHostName = iter.next();
				Host otherHost = hostState.getOtherHosts().get(otherHostName);
				try {
					otherHost.addOtherHost(this, hostState.getName());	
					logger.incrementMessageCount();
				} catch (RemoteException e) {
					//if an exception happens here, one of the other hosts is unavailable as well
					failureDetected(otherHostName);				
				}
			}
			
			logEvent("joined the group");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * Checks for a Signal from an other Host. If he doesn't reply within CHECK_WAIT_TIME
	 * the other host is considered dead and gets reported. 
	 * @param host
	 */
	private void checkOtherHost(String hostName){
		if(!hostState.isCrashed()){
			logEvent("checking " + hostName);
			//synchronized(hostState.getOtherHosts()){
				//get the host from the map containing the other hosts
				Host host = hostState.getOtherHosts().get(hostName);
				
				//start timer 
				Timer t = new Timer();
				t.schedule(new DetectedFailureTask(this, hostName), Host.CHECK_WAIT_TIME);
				
				if(hostState.getCheckTimers() != null){
					//synchronized(hostState.getCheckTimers()){
						//add the timer to the map containing the active timers
						hostState.addCheckTimerForHost(hostName, t);
						//hostState.getCheckTimers().put(hostName, t);
					//}				
				}
				
				
				//send the checkHost request to the other host
				try {
					if(host != null){
						host.checkHost(this, hostState.getName());
						logger.incrementMessageCount();
					}
					//if he gets a response in time, the timer t will be cancelled
				
				} catch (RemoteException e) {
					//if an exception happens here, the host was dead already
					failureDetected(hostName);
				}
			//}	
		}				
	}
	
	/**
	 * Cancels the timer associated with the given hostname
	 * @param hostname
	 */
	private void cancelTimerForHost(String hostName){
		if(hostState.getCheckTimers() != null){
			Timer t = hostState.getCheckTimers().get(hostName);
			if(t != null){
				t.cancel();
				hostState.removeTimerForHost(hostName);
				//hostState.getCheckTimers().remove(hostName);
			}
		}
	}
	
	/**
	 * Cancels the schedule timer associated with the given hostname
	 * @param hostname
	 */
	private synchronized void cancelScheduleTimerForHost(String hostName){
		if(hostState.getCheckScheduleTimers() != null){
			Timer t = hostState.getCheckScheduleTimers().get(hostName);
			if(t != null){
				t.cancel();
				hostState.removeScheduleTimerForHost(hostName);
			}
		}
	}
	
	
	class DetectedFailureTask extends TimerTask{

		private String detectedHostName;
		private HostImpl context;
		
		public DetectedFailureTask(HostImpl context, String hostName){
			super();
			this.detectedHostName = hostName;
			this.context = context;
		}
		public void run() {
			logEvent(detectedHostName + " failed to respnd");
			context.failureDetected(detectedHostName);
		}
		
	}

	@Override
	public String getName() throws RemoteException {
		return hostState.getName();
	}
	
	class CheckForHosts extends TimerTask{
		private HostImpl context;
		
		public CheckForHosts(HostImpl context){
			this.context = context;
		}
		@Override
		public void run() {
			context.checkOtherHosts();		
		}
	}
	
	class CheckForHost extends TimerTask {
		private HostImpl context;
		private String hostName;
		
		public CheckForHost(HostImpl context, String hostName){
			this.context = context;
			this.hostName = hostName;
		}
		@Override
		public void run() {
			if(!hostState.isCrashed()){
				context.checkOtherHost(hostName);
			}
		}
	}

	public void checkOtherHosts(){
		if(!hostState.isCrashed()){
			//synchronized(hostState.getOtherHosts()){			
				HashMap<String, Host> otherHostsCpy = (HashMap<String, Host>) hostState.getOtherHosts().clone();
				Iterator<String> iter = otherHostsCpy.keySet().iterator();
				while(iter.hasNext()){
					checkOtherHost(iter.next());
				}
			//}
			rescheduleChecks();
		}	
	}
	
	private synchronized void rescheduleCheckForHost(String hostName) {
		hostState.addCheckScheduleTimerForHost(hostName, new Timer());
		hostState.getCheckScheduleTimers().get(hostName).schedule(
				new CheckForHost(this, hostName), Host.CHECK_SCHEDULE_INTERVAL);

	}
	
	private synchronized void rescheduleChecks(){		
		if(hostState.getCheckScheduleTimers() == null){
			hostState.setCheckScheduleTimers(new HashMap<String,Timer>());
		}
		HashMap<String, Host> otherHostsCpy = (HashMap<String, Host>) hostState.getOtherHosts().clone();
		Iterator<String> iter = otherHostsCpy.keySet().iterator();
		while(iter.hasNext()){
			rescheduleCheckForHost(iter.next());
		}
	}
	
	/**
	 * Gets called when a failure of a host has been detected. The method informs all other hosts
	 * about the failure by removing the dead host from their lists
	 * @param detectedHostName
	 */
	public void informOtherHosts(String detectedHostName) {
		if(hostState.getOtherHosts() != null){
			synchronized(hostState.getOtherHosts()){
				HashMap<String, Host> otherHostsCpy = (HashMap<String, Host>) hostState.getOtherHosts().clone();
				Iterator<String> iter = otherHostsCpy.keySet().iterator();
				while(iter.hasNext()){
					String otherHostName = iter.next();
					Host otherHost = hostState.getOtherHosts().get(otherHostName);
					try {
						if(otherHost != null){
							otherHost.removeOtherHost(detectedHostName);	
							logger.incrementMessageCount();
						}
					} catch (RemoteException e) {
						//if an exception happens here, one of the other hosts is unavailable as well
						failureDetected(otherHostName);				
					}
				}
			}
		}				
	}
	
	/** gets called whenever a dead Host has been detected
	 * 
	 * @param hostName
	 */
	public void failureDetected(String detectedHostName){
		logEvent("detected failure of Host " + detectedHostName);
			
		//remove detected Host from the map
		try {
			logger.reportCrashedHost(detectedHostName);
			removeOtherHost(detectedHostName);
		} catch (RemoteException e1) {
			e1.printStackTrace();
		}
		//inform the other hosts about the failure
		//informOtherHosts(detectedHostName);
	}
	
	private void logEvent(String eventMsg){
//		System.out.println("[" + new Date(System.currentTimeMillis()).toString() + "]:"
//				+ "Host " + hostState.getName() + ": " + eventMsg);
		if(!hostState.isCrashed())
			hostState.log("[" + new Date(System.currentTimeMillis()).toString() + "]:"
				+ eventMsg + "\n");
	}

	/**
	 * Gets called when a new Host has joined the group.
	 */
	@Override
	public void addOtherHost(Host otherHost, String otherHostName) throws RemoteException{
		if(hostState.getOtherHosts() != null)
			hostState.getOtherHosts().put(otherHostName, otherHost);
		hostState.cancelTimerFor(otherHostName);
		hostState.cancelScheduleTimerFor(otherHostName);
	}

	@Override
	public void removeOtherHost(String detectedHostName) throws RemoteException {
		if(hostState.getOtherHosts() != null)
			hostState.getOtherHosts().remove(detectedHostName);
		//TODO: this correction may have been a very important one!
		cancelTimerForHost(detectedHostName);
		cancelScheduleTimerForHost(detectedHostName);
	}

	/**
	 * 
	 */
	@Override
	public void run() {
		joinGroup();
		rescheduleChecks();
	}

	@Override
	public void checkResponse(String hostName) throws RemoteException {
		logEvent("received response signal from " + hostName);
		//cancel the timer for the host
		cancelTimerForHost(hostName);
		//cancel schedule timer for host
		cancelScheduleTimerForHost(hostName);
		//reset schedule Timer for host
		rescheduleCheckForHost(hostName);
	}

}
